package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 根据id，返回数据。
     * queryWithMutex：【缓存穿透-空字符串返回】 + 【缓存击穿-互斥锁】
     * @param id
     * @return
     */
    @Override
    public Result selectById(Long id) {
        //1.缓存穿透，直接调用下面方法【queryWithPassThrough】即可
        //Shop shop = queryWithPassThrough(id);

        //2. 缓存击穿：利用互斥锁。考虑了缓存穿透，返回null值
        //Shop shop = queryWithMutex(id);

        // 3. 缓存击穿 逻辑过期。 不考虑缓存穿透，所有的值都存在。
        Shop shop = queryWithLogicalExpire(id);

         /*
         在方法内，由于返回值是Shop类型，所以返回直接设为了null。
         那么在这里做一下处理，当返回空时，后端友好提示一下返回结果。
         */
        if (shop ==null) {
            return Result.fail("店铺不存在");
        }
        // . 返回数据结果
        return Result.ok(shop);
    }

    /**
     * 用逻辑过期---代码实现；
     * @param id
     * @return
     */
    public Shop queryWithLogicalExpire(Long id){
        //1. 通过id 到redis查询
        String shopKey = CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        //2. 判断redis中是否命中
        if (StrUtil.isBlank(shopJson)){
            //3.如果 未命中，直接返回
            return null;
        }
        // 4.如果查到了数据：判断是否过期。那么就需要先把JSON反序列化为对象
        /*得到的bean是RedisData*/
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject)redisData.getData();
        //得到了数据，并转为了bean
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            // 5.1如果没过期，直接返回数据
            return shop;
        }
        //5.2 数据已经过期，进行缓存重建 。
        //6.1 尝试获取互斥锁.
        String lockKey =LOCK_SHOP_KEY+id;
        boolean isLock = tryLock(lockKey);
        // 6.2 判断是否拿到了互斥锁
        if (isLock){
            // 6.3 成功，开启独立线程，实现缓存重建（去数据库读取数据，并更新数据，释放线程锁）
            CACHE_REBUILD_EXECUTOR.submit(()->{
                //为了方便测试，添加商品数据过期时间为20秒。
                try {
                    this.save2ShopRedis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 必须要释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4 获取互斥锁失败，返回过期的商品信息
        return shop;
    }

    // 创建线程池：
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);


    /**
     * mutex：互斥锁的意思
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id){
        //1. 通过id 到redis查询
        String shopKey = CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        //2. 判断有没有查询到. 不是空的--》有数据则返回结果
        // isNOtBlank只有一种情况，真正有数据才能为true。 比如空字符串是false
        if (StrUtil.isNotBlank(shopJson)){
            // 3. 如果查到了则直接返回结果，说明查到了JSON，需要转换为实体类
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //  缓存穿透后，将空字符串保存在缓存中，再次查询时，将缓存返回空数据
        if (shopJson != null){
            return null;
        }
        // 应该给锁设置try，即使抛异常，也必须要要释放锁。
        String lockKey= null;
        Shop shop = null;

        try {
            //4. 如果缓存没有查询到，进入缓存重建
            // 4.1 获取互斥锁
            lockKey = "lock:shop:"+id;
            boolean isLock = tryLock(lockKey);
            // 4.2 判断互斥锁是否获取成功
            if (!isLock){
                // 4.3 如果没有拿到互斥锁，休眠等待+ 然后从新去缓存中查数据
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            // 4.4 如果拿到了互斥锁，去数据进行查询数据
            shop = getById(id);
            // 模拟提高缓存重建时间。提高查询数据的时间
            Thread.sleep(200);
            // 5. 判断有没有在数据库中，取到数据。
            if (shop == null){
                // 缓存穿透问题：如果没有拿到数据： 前端要查id=shopKey， 如果数据库没有，则向缓存中保存“”空字符串，并过期时间2分钟。
                stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null ;
            }
            //6. 数据库查询到了，则写入redis
            String shopStr = JSONUtil.toJsonStr(shop);
            //  设置超时剔除，CACHE_SHOP_TTL=30L分钟后自动从缓存中删除
            stringRedisTemplate.opsForValue().set(shopKey,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            //是打断的异常
            throw new RuntimeException(e);
        } finally {
            // 7.释放互斥锁
            unlock(lockKey);
        }
        //8. 返回数据结果
        return shop;
    }


    /**
     * 缓存穿透
     */
    public Shop queryWithPassThrough(Long id){
        //1. 通过id 到redis查询
        String shopKey = CACHE_SHOP_KEY+id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
        //2. 判断有没有查询到. 不是空的--》有数据则返回结果
        // isNOtBlank只有一种情况，真正有数据才能为true。 比如空字符串是false
        if (StrUtil.isNotBlank(shopJson)){
            // 3. 如果查到了则直接返回结果，说明查到了JSON，需要转换为实体类
            return JSONUtil.toBean(shopJson, Shop.class);

        }
        //  缓存穿透后，将空字符串保存在缓存中，再次查询时，将缓存返回空数据
        if (shopJson != null){
            return null;
        }
        //4. 如果没有查到，到数据查询
        Shop shop = getById(id);
        // 5. 数据库没有查询到，返回错误信息
        if (shop == null){
            // 前端要查id=shopKey， 如果数据库没有，则向缓存中保存“”空字符串，并过期时间2分钟。
            stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null ;
        }
        //6. 数据库查询到了，则写入redis
        String shopStr = JSONUtil.toJsonStr(shop);
        //  设置超时剔除，CACHE_SHOP_TTL=30L分钟后自动从缓存中删除
        stringRedisTemplate.opsForValue().set(shopKey,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //7. 返回数据结果
        return shop;
    }


    /**
     * 更新商品信息； 先更新数据库，然后在删除缓存信息，、
     *      当查询时，在从数据库查数据，在放入缓存中。
     * @param shop
     */
    @Override
    @Transactional
    public Result updateShop(Shop shop) {
        //1. 更新据库信息
        /*获取更新商品的id*/
        Long id = shop.getId();
        if (id == null){
            return Result.fail("id不能为空");
        }
        // 判断是否更新成功
        boolean b = updateById(shop);
        if (!b){
            return Result.fail("更新失败");
        }
        //2. 删除缓存信息
        stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
        return Result.ok();
    }

    /**
     * 获取锁
     * @param key
     * @return
     */
    private boolean tryLock(String key){
        // setnx: setIfAbsent 如果不存在，才会存的意思。 10秒内过期
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        /**
         * 如果直接返回 return aboolean; 会进行拆箱，那么可能空指针
         */
        return BooleanUtil.isTrue(aBoolean);

    }
    /**
     * 释放锁
     * @parm key
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

    /**
     * 模拟缓存预热功能，将一个热点key，设为逻辑过期时间，写入到redis缓存中。
     * @param id
     * @param expireSeconds
     */
    public void save2ShopRedis(Long id,Long expireSeconds) throws InterruptedException {
        //商铺的实体类
        Shop shop = getById(id);
        // 缓存重建有一定的延迟时间。
        Thread.sleep(200);
        // 用来封装商铺实体类和逻辑过期时间
        RedisData redisData = new RedisData();
        // 设置数据
        redisData.setData(shop);
        //设置逻辑过期时间
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //存入redis中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }
}
